package com.wdl.webserver;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang.StringUtils;
import org.json.JSONObject;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.keycloak.representations.AccessToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.wdl.webserver.api.KeycloakAdminController;

@Component
public class ZuulFilters extends ZuulFilter {
    public static final String INST_NAME_FOR_USER_ROLE = "INST_NAME_FOR_MOBILE_USER_ROLE";
    public static final String INST_NAME_FOR_HUSHITONG = "护适通";

    @Autowired
    KeycloakAdminController keycloakAdmin;

    private static final Logger LOG = LoggerFactory.getLogger("ZuulFilter");

    public String filterType() {
        return "pre";
    }

    public int filterOrder() {
        return 7;
    }

    public boolean shouldFilter() {
        RequestContext context = RequestContext.getCurrentContext();
        return context.get("proxy").equals("backend") || context.get("proxy").equals("prometheus");
    }

    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        if (context.getRequest().getMethod().equals(HttpMethod.OPTIONS.toString())) {
            return null;
        }

        logRequest(context);

        return null;
    }

    private void logRequest(RequestContext context) {
        HttpServletRequest request = context.getRequest();

        String uri = "";
        try {
            uri = URLDecoder.decode(request.getRequestURI(), "UTF-8");
            if (uri.startsWith("/api/v1/") || uri.equals("/data/institution/search/name")) {
                // For pro requests, has filtered before, just skip here!
                return;
            }
        } catch (UnsupportedEncodingException e) {
            LOG.error("UnsupportedEncodingException happened", e);
            return;
        }

        String user = SecurityContextHolder.getContext().getAuthentication().getName();

        String institutionName = getInstitutionNameFromAuthUser(request);
        if (StringUtils.isEmpty(institutionName)) {
            LOG.error("Failed to get institution name from request - User: [{}], Method: [{}], uri: [{}]", user, request.getMethod(), uri);
            context.setSendZuulResponse(false);  
            context.setResponseStatusCode(401);  
            context.setResponseBody("{\"result\":\"User [" + user + "] has no institution specified!\"}");  
            context.set("isSuccess", false);  
            return;
        } else if (INST_NAME_FOR_HUSHITONG.equals(institutionName)) {
            // For hushitong user, allow all
            return;
        } else if (INST_NAME_FOR_USER_ROLE.equals(institutionName)) {
            // the institution name in request must be "护适通", so force it here for late compare!
            institutionName = INST_NAME_FOR_HUSHITONG;
        }

        String requestInstName = getInstitutionNameFromRequest(request, uri);
        if (StringUtils.isEmpty(requestInstName)) {
            LOG.warn("Couldn't get institution name for this request - method: [{}], uri: [{}]", request.getMethod(), uri);
            return;
        }

        LOG.info("User[{}] from institution [{}], Operation: [{}], uri: [{}]", user, institutionName, request.getMethod(), uri);

        if (!institutionName.equals(requestInstName)) {
            LOG.error("User[{}] from institution [{}], Operation: [{}] for [{}], uri: [{}]", user, institutionName, request.getMethod(), requestInstName, uri);
            context.setSendZuulResponse(false);  
            context.setResponseStatusCode(401);  
            context.setResponseBody("{\"result\":\"User [" + user + "] belongs to other institution!\"}");  
            context.set("isSuccess", false);  
            return; 
        }
    }

    @SuppressWarnings("resource")
    static public String extractPostRequestBody(HttpServletRequest request) {
        if ("POST".equalsIgnoreCase(request.getMethod()) || "PUT".equalsIgnoreCase(request.getMethod()) || "PATCH".equalsIgnoreCase(request.getMethod())) {
            Scanner s = null;
            try {
                s = new Scanner(request.getInputStream(), "UTF-8").useDelimiter("\\A");
            } catch (IOException e) { }
            return s.hasNext() ? s.next() : "";
        }

        return "";
    }

    public static String getInstitutionNameFromRequest(HttpServletRequest request, String uri) {
        Object instIdObj = request.getParameter("institutionId");
        String requestInstId = instIdObj == null ? "" : String.valueOf(instIdObj);

        Object personIdObj = request.getParameter("personId");
        String personId = personIdObj == null ? "" : String.valueOf(personIdObj);

        String requestInstName = "";
        if (!StringUtils.isEmpty(requestInstId)) {
            requestInstName = getInstitutionNameFromInstitutionId(requestInstId);
        } else if (!StringUtils.isEmpty(personId)) {
            requestInstName = getInstitutionNameFromPersonId(personId);
        } else if (uri.startsWith("/data/person/")) {
            Pattern pattern = Pattern.compile("^/data/person/([0-9]+)$");
            Matcher m = pattern.matcher(uri);
            if (m.find()) {
                personId = m.group(1);
                requestInstName = getInstitutionNameFromPersonId(personId);
            } else {
                requestInstName = getInstitutionNameFromBody(request, uri);
            }
        } else if (uri.startsWith("/data/device/")) {
            Pattern pattern = Pattern.compile("^/data/device/([0-9]+)$");
            Matcher m = pattern.matcher(uri); 
            if (m.find()) {
                String deviceId = m.group(1);
                requestInstName = getInstitutionNameFromDeviceId(deviceId);
            } else {
                requestInstName = getInstitutionNameFromBody(request, uri);
            }
        } else if (uri.startsWith("/data/department/")) {
            Pattern pattern = Pattern.compile("^/data/department/([0-9]+)$");
            Matcher m = pattern.matcher(uri); 
            if (m.find()) {
                String departmentId = m.group(1);
                requestInstName = getInstitutionNameFromDepartmentId(departmentId);
            } else {
                requestInstName = getInstitutionNameFromBody(request, uri);
            }
        } else if (uri.startsWith("/data/nurse/")) {
            Pattern pattern = Pattern.compile("^/data/nurse/([0-9]+)$");
            Matcher m = pattern.matcher(uri); 
            if (m.find()) {
                String nurseId = m.group(1);
                requestInstName = getInstitutionNameFromNurseId(nurseId);
            } else {
                requestInstName = getInstitutionNameFromBody(request, uri);
            }
        } else if (uri.startsWith("/data/building/")) {
            Pattern pattern = Pattern.compile("^/data/building/([0-9]+)$");
            Matcher m = pattern.matcher(uri); 
            if (m.find()) {
                String buildingId = m.group(1);
                requestInstName = getInstitutionNameFromBuildingId(buildingId);
            } else if (uri.endsWith("/levels")) {
                pattern = Pattern.compile("^/data/building/([0-9]+)/levels");
                m = pattern.matcher(uri); 
                if (m.find()) {
                    String buildingId = m.group(1);
                    requestInstName = getInstitutionNameFromBuildingId(buildingId);
                }
            } else {
                requestInstName = getInstitutionNameFromBody(request, uri);
            }
        } else if (uri.startsWith("/data/alarm/")) {
            Pattern pattern = Pattern.compile("^/data/alarm/([0-9]+)$");
            Matcher m = pattern.matcher(uri); 
            if (m.find()) {
                String alarmId = m.group(1);
                requestInstName = getInstitutionNameFromAlarmId(alarmId);
            }
        } else if (uri.startsWith("/data/institution/")) {
            Pattern pattern = Pattern.compile("^/data/institution/([0-9]+)([/.*]*)");
            Matcher m = pattern.matcher(uri); 
            if (m.find()) {
                String institutionId = m.group(1);
                requestInstName = getInstitutionNameFromInstitutionId(institutionId);
            }
        } else if (uri.startsWith("/data/ackAlarm")) {
            Object alarmIdObj = request.getParameter("alarmId");
            String alarmId = alarmIdObj == null ? "" : String.valueOf(alarmIdObj);

            if (!StringUtils.isEmpty(alarmId)) {
                requestInstName = getInstitutionNameFromAlarmId(alarmId);
            }
        } else if (uri.equals("/data/configData")) {
            String body = extractPostRequestBody(request);
            if (!StringUtils.isEmpty(body)) {
                JSONObject jsonObj = new JSONObject(body);
                personId = String.valueOf(jsonObj.get("personId"));
                String type = String.valueOf(jsonObj.get("type"));
                if ("delete".equals(type)) {
                    LOG.info("Update configData after person [{}] removal, allow it!", personId);
                } else {
                    requestInstName = getInstitutionNameFromPersonId(personId);
                }
            }
        }

        return requestInstName;
    }

    public static String getInstitutionNameFromBody(HttpServletRequest request, String uri) {
        if (uri.endsWith("/institution")) {
            String body = extractPostRequestBody(request);
            if (StringUtils.isEmpty(body)) {
                return "";
            }

            Pattern pattern = Pattern.compile("^data/institution/([0-9]+)$");
            Matcher m = pattern.matcher(body); 
            if (m.find()) {
                String institutionId = m.group(1);
                return getInstitutionNameFromInstitutionId(institutionId);
            }
        }

        return "";
    }

    @SuppressWarnings("rawtypes")
    public static String getInstitutionNameFromAuthUser(HttpServletRequest request) {
        KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) request.getUserPrincipal();
        KeycloakPrincipal principal= (KeycloakPrincipal) token.getPrincipal();
        KeycloakSecurityContext session = principal.getKeycloakSecurityContext();
        AccessToken accessToken = session.getToken();
        String institutionName = accessToken.getGivenName();
        if (StringUtils.isEmpty(institutionName)) {
            // must be "user" role (or super), requests must be for 护适通!
            String user = SecurityContextHolder.getContext().getAuthentication().getName();
            if ("super".equals(user)) {
                return INST_NAME_FOR_HUSHITONG;
            } else {
                return INST_NAME_FOR_USER_ROLE;
            }
        }

        institutionName = institutionName.substring(KeycloakAdminController.INSTITUTION_PREFIX.length());
        if (LOG.isDebugEnabled()) {
            LOG.info("Institution Name is " + institutionName);
        }

        return institutionName;
    }

    public static String getInstitutionNameFromInstitutionId(String institutionId) {
        MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
        headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
        headers.add(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.name());

        RestTemplate restTemplate = new RestTemplate();
        restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
        HttpEntity<String> instRequest = new HttpEntity<>(null, headers);

        String resp = restTemplate.exchange("http://localhost:8088/data/institution/" + institutionId, HttpMethod.GET, instRequest, String.class).getBody();
        if (StringUtils.isEmpty(resp)) {
            // couldn't find which institution the user belongs to
            return "";
        }

        JSONObject jsonObj = new JSONObject(resp);
        String instName = String.valueOf(jsonObj.get("instName"));
        return instName;
    }

    public static String getInstitutionNameFromPersonId(String personId) {
        MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
        headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
        headers.add(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.name());

        RestTemplate restTemplate = new RestTemplate();
        restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
        HttpEntity<String> request = new HttpEntity<>(null, headers);

        String resp = restTemplate.exchange("http://localhost:8088/data/person/" + personId + "/institution", HttpMethod.GET, request, String.class).getBody();
        if (StringUtils.isEmpty(resp)) {
            // couldn't find which institution the user belongs to
            return "";
        }

        JSONObject jsonObj = new JSONObject(resp);
        String instName = String.valueOf(jsonObj.get("instName"));
        return instName;
    }

    public static String getInstitutionNameFromDeviceId(String deviceId) {
        MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
        headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
        headers.add(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.name());

        RestTemplate restTemplate = new RestTemplate();
        restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
        HttpEntity<String> request = new HttpEntity<>(null, headers);

        String resp = restTemplate.exchange("http://localhost:8088/data/device/" + deviceId + "/institution", HttpMethod.GET, request, String.class).getBody();
        if (StringUtils.isEmpty(resp)) {
            // couldn't find which institution the user belongs to
            return "";
        }

        JSONObject jsonObj = new JSONObject(resp);
        String instName = String.valueOf(jsonObj.get("instName"));
        return instName;
    }

    public static String getInstitutionNameFromDepartmentId(String departmentId) {
        MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
        headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
        headers.add(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.name());

        RestTemplate restTemplate = new RestTemplate();
        restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
        HttpEntity<String> request = new HttpEntity<>(null, headers);

        String resp = restTemplate.exchange("http://localhost:8088/data/department/" + departmentId + "/institution", HttpMethod.GET, request, String.class).getBody();
        if (StringUtils.isEmpty(resp)) {
            // couldn't find which institution the user belongs to
            return "";
        }

        JSONObject jsonObj = new JSONObject(resp);
        String instName = String.valueOf(jsonObj.get("instName"));
        return instName;
    }

    public static String getInstitutionNameFromNurseId(String nurseId) {
        MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
        headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
        headers.add(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.name());

        RestTemplate restTemplate = new RestTemplate();
        restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
        HttpEntity<String> request = new HttpEntity<>(null, headers);

        String resp = restTemplate.exchange("http://localhost:8088/data/nurse/" + nurseId + "/institution", HttpMethod.GET, request, String.class).getBody();
        if (StringUtils.isEmpty(resp)) {
            // couldn't find which institution the user belongs to
            return "";
        }

        JSONObject jsonObj = new JSONObject(resp);
        String instName = String.valueOf(jsonObj.get("instName"));
        return instName;
    }

    public static String getInstitutionNameFromBuildingId(String buildingId) {
        MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
        headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
        headers.add(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.name());

        RestTemplate restTemplate = new RestTemplate();
        restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
        HttpEntity<String> request = new HttpEntity<>(null, headers);

        String resp = restTemplate.exchange("http://localhost:8088/data/building/" + buildingId + "/institution", HttpMethod.GET, request, String.class).getBody();
        if (StringUtils.isEmpty(resp)) {
            // couldn't find which institution the user belongs to
            return "";
        }

        JSONObject jsonObj = new JSONObject(resp);
        String instName = String.valueOf(jsonObj.get("instName"));
        return instName;
    }

    public static String getInstitutionNameFromAlarmId(String alarmId) {
        MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
        headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
        headers.add(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.name());

        RestTemplate restTemplate = new RestTemplate();
        restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
        HttpEntity<String> request = new HttpEntity<>(null, headers);

        String resp = restTemplate.exchange("http://localhost:8088/data/alarm/" + alarmId, HttpMethod.GET, request, String.class).getBody();
        if (StringUtils.isEmpty(resp)) {
            return "";
        }

        JSONObject jsonObj = new JSONObject(resp);
        String institutionId = String.valueOf(jsonObj.get("institutionId"));

        resp = restTemplate.exchange("http://localhost:8088/data/institution/" + institutionId, HttpMethod.GET, request, String.class).getBody();
        if (StringUtils.isEmpty(resp)) {
            // couldn't find which institution the user belongs to
            return "";
        }

        jsonObj = new JSONObject(resp);
        String instName = String.valueOf(jsonObj.get("instName"));
        return instName;
    }

    public static void validatePersonId(HttpServletRequest request, Object personId, String uri) {
        String instNameInUser = ZuulFilters.getInstitutionNameFromAuthUser(request);
        if (StringUtils.isEmpty(instNameInUser)) {
            LOG.error("Failed to get institution name from request - personId: [{}], uri: [{}]", personId, uri);
            throw new AuthenticationServiceException("User is empty, personId=" + personId + ", uri=" + uri);
        } else if (INST_NAME_FOR_HUSHITONG.equals(instNameInUser)) {
            // For hushitong user, allow all
            return;
       } else if (INST_NAME_FOR_USER_ROLE.equals(instNameInUser)) {
           // the institution name in request must be "护适通"! That is, this user can only access the Hushitong resources!
           instNameInUser = INST_NAME_FOR_HUSHITONG;
       }

        String instNameInRequest = ZuulFilters.getInstitutionNameFromPersonId(String.valueOf(personId));
        if (StringUtils.isEmpty(instNameInRequest)) {
            LOG.error("Cannot get institution name from request for [{}]!", uri);
            return;
        }

        if (!instNameInUser.equals(instNameInRequest)) {
            LOG.error("User in [{}] cannot perform operation in [{}]!", instNameInUser, instNameInRequest);
            throw new AuthenticationServiceException("User in " + instNameInUser + " cannot perform operations in " + instNameInRequest);
        }
    }

    public static void validateInstitutionId(HttpServletRequest request, Object institutionId, String uri) {
        String instNameInUser = ZuulFilters.getInstitutionNameFromAuthUser(request);
        if (StringUtils.isEmpty(instNameInUser)) {
            LOG.error("Failed to get institution name from request - institutionId: [{}], uri: [{}]", institutionId, uri);
            throw new AuthenticationServiceException("User is empty, institutionId=" + institutionId + ", uri=" + uri);
        } else if (INST_NAME_FOR_HUSHITONG.equals(instNameInUser)) {
            // For hushitong user, allow all
            return;
       } else if (INST_NAME_FOR_USER_ROLE.equals(instNameInUser)) {
           // the institution name in request must be "护适通"!
           instNameInUser = INST_NAME_FOR_HUSHITONG;
       }

        String instNameInRequest = ZuulFilters.getInstitutionNameFromInstitutionId(String.valueOf(institutionId));
        if (StringUtils.isEmpty(instNameInRequest)) {
            LOG.error("Cannot get institution name from request for [{}]!", uri);
            return;
        }

        if (!instNameInUser.equals(instNameInRequest)) {
            LOG.error("User in [{}] cannot perform operation in [{}]!", instNameInUser, instNameInRequest);
            throw new AuthenticationServiceException("User in " + instNameInUser + " cannot perform operations in " + instNameInRequest);
        }
    }
}